Skip to content

[v9]話者選択機能の実装#242

Open
ibuki-hum4 wants to merge 2 commits intofeat/gofrom
ibuki-hum4/issue211
Open

[v9]話者選択機能の実装#242
ibuki-hum4 wants to merge 2 commits intofeat/gofrom
ibuki-hum4/issue211

Conversation

@ibuki-hum4
Copy link
Member

Overview

  • /tts set voiceの追加
    • 1ページ話者種類20個まで(現在7ページ(voicevox-ver:0.16.0))

issue

#211

Review request

@yuito-it
etc...

@ibuki-hum4 ibuki-hum4 requested a review from a team as a code owner February 4, 2026 12:34
Copilot AI review requested due to automatic review settings February 4, 2026 12:34
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements a speaker selection feature for the text-to-speech (TTS) functionality, allowing users to choose from available voices through a paginated UI. The implementation adds a new /tts set voice command that displays speakers in pages of up to 20 options with navigation buttons.

Changes:

  • Added /tts set voice command with paginated speaker selection UI
  • Implemented VoiceVox API client method to fetch available speakers
  • Added message component handlers for speaker selection and page navigation

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
src/internal/bot/messageComponent/tts_voice.go New message component handlers for speaker selection and pagination
src/internal/bot/command/general/tts/set/voice.go New command implementation for voice selection with speaker fetching and UI building
src/internal/bot/command/general/tts/set.go New subcommand group for TTS settings
src/internal/bot/command/general/tts.go Registered the new set subcommand group
src/internal/bot/handler/interaction.go Added special handling to mark tts set voice as ephemeral
src/internal/bot/command/general/help/help-commands.go Updated help documentation to include the new voice command
src/internal/api/voicevox/types.go Added Speaker and SpeakerStyle types for API responses
src/internal/api/voicevox/client.go Added GetSpeakers method to fetch available speakers from VoiceVox API

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 126 to 140
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseUpdateMessage,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Title: "話者設定を更新しました",
Description: "選択した話者: " + label,
Color: config.Colors.Success,
Timestamp: time.Now().Format(time.RFC3339),
},
},
Components: []discordgo.MessageComponent{},
},
})
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling. The return value of s.InteractionRespond is ignored. While this might be acceptable for some scenarios, errors from InteractionRespond should ideally be logged for debugging purposes, especially since the function logs other errors. Consider logging the error if InteractionRespond fails, similar to how other errors in this file are handled.

Copilot uses AI. Check for mistakes.
@yuito-it yuito-it linked an issue Feb 4, 2026 that may be closed by this pull request
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +34 to +41
func FetchSpeakers(ctx *internal.BotContext) ([]voicevox.Speaker, error) {
cachedSpeakers.mu.RLock()
if time.Now().Before(cachedSpeakers.expires) && len(cachedSpeakers.speakers) > 0 {
speakers := cachedSpeakers.speakers
cachedSpeakers.mu.RUnlock()
return speakers, nil
}
cachedSpeakers.mu.RUnlock()
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache check and early return logic has a potential race condition. After releasing the read lock and before acquiring the write lock, another goroutine might have already fetched and cached the speakers. This could lead to redundant API calls.

Consider using a double-checked locking pattern where you recheck the cache after acquiring the write lock but before making the API call, or use sync.Once for initialization.

Copilot uses AI. Check for mistakes.
}
cachedSpeakers.mu.RUnlock()

requestCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The context passed to GetSpeakers is created from context.Background(), ignoring the potential cancellation context that may be available from the bot context. If the bot is shutting down or the request should be cancelled, this independent context won't be affected.

Consider accepting a context.Context parameter in FetchSpeakers or using a context from the BotContext if available.

Suggested change
requestCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
// Derive the request context from the bot context if it implements context.Context,
// otherwise fall back to context.Background().
baseCtx := context.Background()
if ctx != nil {
if parentCtx, ok := any(ctx).(context.Context); ok && parentCtx != nil {
baseCtx = parentCtx
}
}
requestCtx, cancel := context.WithTimeout(baseCtx, 30*time.Second)

Copilot uses AI. Check for mistakes.
return
}

speakerID := values[0]
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The speakerID from the select menu is directly used without validation. While Discord's select menu UI prevents arbitrary input during normal operation, it's a security best practice to validate that the selected speaker ID actually exists before storing it in the database.

Consider adding a validation step that checks if the speakerID exists in the speaker list, similar to how ResolveSpeakerLabel already iterates through speakers to find a match.

Suggested change
speakerID := values[0]
speakerID := values[0]
// Validate that the selected speakerID actually exists in the select menu options.
validSpeaker := false
if i.Message != nil {
for _, row := range i.Message.Components {
actionRow, ok := row.(*discordgo.ActionsRow)
if !ok {
continue
}
for _, comp := range actionRow.Components {
selectMenu, ok := comp.(*discordgo.SelectMenu)
if !ok {
continue
}
if selectMenu.CustomID != ttsutil.VoiceSelectCustomID {
continue
}
for _, opt := range selectMenu.Options {
if opt.Value == speakerID {
validSpeaker = true
break
}
}
if validSpeaker {
break
}
}
if validSpeaker {
break
}
}
}
if !validSpeaker {
log.Println("HandleTTSSetVoice: invalid speakerID selected:", speakerID)
if err := s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Embeds: []*discordgo.MessageEmbed{
{
Title: "エラー",
Description: "不正な話者IDが選択されました。もう一度お試しください。",
Color: config.Colors.Error,
Timestamp: time.Now().Format(time.RFC3339),
},
},
Flags: discordgo.MessageFlagsEphemeral,
},
}); err != nil {
log.Println("Failed to respond interaction:", err)
}
return
}

Copilot uses AI. Check for mistakes.

cachedSpeakers.mu.Lock()
cachedSpeakers.speakers = speakers
cachedSpeakers.expires = time.Now().Add(5 * time.Minute)
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache expiration time 5 * time.Minute is hardcoded. For consistency with the codebase (see src/internal/util/dictionary_cache.go which defines DefaultCacheTTL = 5 * time.Minute as a constant), consider extracting this as a named constant like speakerCacheTTL.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[v9] 話者選択機能

1 participant